diff --git a/bulk-edit-cron-offload.php b/bulk-edit-cron-offload.php
index 4aedcab57da06cc71bc9f073b793768d16ade82b..3f1f58df05cd7a072daf7a94e3b54080d7dfa588 100644
--- a/bulk-edit-cron-offload.php
+++ b/bulk-edit-cron-offload.php
@@ -20,5 +20,6 @@ require __DIR__ . '/includes/utils.php';
 // Plugin functionality.
 require __DIR__ . '/includes/class-main.php';
 require __DIR__ . '/includes/class-delete-all.php';
+require __DIR__ . '/includes/class-delete-permanently.php';
 require __DIR__ . '/includes/class-move-to-trash.php';
 require __DIR__ . '/includes/class-restore-from-trash.php';
diff --git a/includes/class-delete-permanently.php b/includes/class-delete-permanently.php
new file mode 100644
index 0000000000000000000000000000000000000000..c56a137208007beaac7ea90e2b6bc81a55f66044
--- /dev/null
+++ b/includes/class-delete-permanently.php
@@ -0,0 +1,157 @@
+<?php
+/**
+ * Offload "Delete Permanently"
+ *
+ * @package Bulk_Edit_Cron_Offload
+ */
+
+namespace Automattic\WP\Bulk_Edit_Cron_Offload;
+
+/**
+ * Class Delete_Permanently
+ */
+class Delete_Permanently {
+	/**
+	 * Class constants
+	 */
+	const ACTION = 'delete';
+
+	const ADMIN_NOTICE_KEY = 'bulk_edit_cron_offload_delete_permanently';
+
+	/**
+	 * Register this bulk process' hooks
+	 */
+	public static function register_hooks() {
+		add_action( Main::build_hook( self::ACTION ), array( __CLASS__, 'process' ) );
+		add_action( Main::build_cron_hook( self::ACTION ), array( __CLASS__, 'process_via_cron' ) );
+
+		add_action( 'admin_notices', array( __CLASS__, 'admin_notices' ) );
+		add_filter( 'posts_where', array( __CLASS__, 'hide_posts_pending_delete' ), 999, 2 );
+	}
+
+	/**
+	 * Handle a request to delete selected posts from the trash
+	 *
+	 * @param object $vars Bulk-request variables.
+	 */
+	public static function process( $vars ) {
+		$action_scheduled = Main::next_scheduled( $vars );
+
+		if ( empty( $action_scheduled ) ) {
+			Main::schedule_processing( $vars );
+			Main::do_admin_redirect( self::ADMIN_NOTICE_KEY, true );
+		} else {
+			Main::do_admin_redirect( self::ADMIN_NOTICE_KEY, false );
+		}
+	}
+
+	/**
+	 * Cron callback to move requested items to trash
+	 *
+	 * @param object $vars Bulk-request variables.
+	 */
+	public static function process_via_cron( $vars ) {
+		$count = 0;
+
+		if ( is_array( $vars->posts ) && ! empty( $vars->posts ) ) {
+			require_once ABSPATH . '/wp-admin/includes/post.php';
+
+			$deleted    = array();
+			$locked     = array();
+			$auth_error = array();
+			$error      = array();
+
+			foreach ( $vars->posts as $post_id ) {
+				// Can the user delete this post?
+				if ( ! user_can( $vars->user_id, 'delete_post', $post_id ) ) {
+					$auth_error[] = $post_id;
+					continue;
+				}
+
+				// Post is locked by someone, so leave it alone.
+				if ( false !== wp_check_post_lock( $post_id ) ) {
+					$locked[] = $post_id;
+					continue;
+				}
+
+				// Try deleting.
+				$post_deleted = wp_delete_post( $post_id );
+				if ( $post_deleted ) {
+					$deleted[] = $post_id;
+				} else {
+					$error[] = $post_id;
+				}
+
+				// Take a break periodically.
+				if ( 0 === $count++ % 50 ) {
+					stop_the_insanity();
+					sleep( 3 );
+				}
+			}
+
+			$results = compact( 'deleted', 'locked', 'auth_error', 'error' );
+			do_action( 'bulk_edit_cron_offload_move_to_trash_request_completed', $results, $vars );
+		} else {
+			do_action( 'bulk_edit_cron_offload_move_to_trash_request_no_posts', $vars->posts, $vars );
+		}
+	}
+
+	/**
+	 * Let the user know what's going on
+	 */
+	public static function admin_notices() {
+		$screen = get_current_screen();
+
+		$type   = '';
+		$message = '';
+
+		if ( isset( $_REQUEST[ self::ADMIN_NOTICE_KEY ] ) ) {
+			if ( 1 === (int) $_REQUEST[ self::ADMIN_NOTICE_KEY ] ) {
+				$type    = 'success';
+				$message = __( 'Success! The selected posts will be deleted shortly.', 'bulk-edit-cron-offload' );
+			} else {
+				$type    = 'error';
+				$message = __( 'The selected posts are already scheduled to be deleted.', 'bulk-edit-cron-offload' );
+			}
+		} elseif ( 'edit' === $screen->base && isset( $_REQUEST['post_status'] ) && 'trash' === $_REQUEST['post_status'] ) {
+			if ( Main::get_post_ids_for_pending_events( self::ACTION, $screen->post_type, 'trash' ) ) {
+				$type    = 'warning';
+				$message = __( 'Some items that would normally be shown here are waiting to be deleted permanently. These items are hidden until then.', 'bulk-edit-cron-offload' );
+			}
+		}
+
+		Main::render_admin_notice( $type, $message );
+	}
+
+	/**
+	 * When a delete is pending for a given post type, hide those posts in the admin
+	 *
+	 * @param string $where Posts' WHERE clause.
+	 * @param object $q WP_Query object.
+	 * @return string
+	 */
+	public static function hide_posts_pending_delete( $where, $q ) {
+		if ( ! is_admin() || ! $q->is_main_query() ) {
+			return $where;
+		}
+
+		if ( 'edit' !== get_current_screen()->base ) {
+			return $where;
+		}
+
+		if ( 'trash' === $q->get( 'post_status' ) ) {
+			return $where;
+		}
+
+		$post__not_in = Main::get_post_ids_for_pending_events( self::ACTION, $q->get( 'post_type' ), $q->get( 'post_status' ) );
+
+		if ( ! empty( $post__not_in ) ) {
+			$post__not_in = implode( ',', $post__not_in );
+			$where       .= ' AND ID NOT IN(' . $post__not_in . ')';
+		}
+
+		return $where;
+	}
+}
+
+Delete_Permanently::register_hooks();
diff --git a/includes/class-main.php b/includes/class-main.php
index 00b958f26d64ec7b0dcb5739d2ef3ea4849c5507..3d4e3c13224cb99c17d245194effff778ad9f5f6 100644
--- a/includes/class-main.php
+++ b/includes/class-main.php
@@ -151,7 +151,7 @@ class Main {
 	 */
 	public static function bulk_action_allowed( $action ) {
 		$allowed_actions = array(
-			'delete', // TODO: "Delete permantently" in Trash.
+			'delete', // class Delete_Permanently.
 			'delete_all', // class Delete_All.
 			'edit',
 			'trash', // class Move_To_trash.
@@ -257,7 +257,7 @@ class Main {
 	/**
 	 * Find the next scheduled instance of a given action, regardless of arguments
 	 *
-     * @param string $bulk_action Bulk action to filter by.
+	 * @param string $bulk_action Bulk action to filter by.
 	 * @param  string $post_type Post type hook is scheduled for.
 	 * @return array
 	 */