diff --git a/app/components/samples/table_component.html.erb b/app/components/samples/table_component.html.erb
index 466c638727..7d71b29bf5 100644
--- a/app/components/samples/table_component.html.erb
+++ b/app/components/samples/table_component.html.erb
@@ -185,10 +185,9 @@
                 <% if @row_actions[:destroy] %>
                   <%= link_to(
                     t(:"projects.samples.index.remove_button"),
-                    project_sample_path(sample.project, sample),
+                    new_namespace_project_samples_deletion_path(sample_id: sample.id, deletion_type: 'single'),
                     data: {
-                      turbo_method: :delete,
-                      turbo_confirm: t(:"projects.samples.index.remove_button_confirmation"),
+                      "turbo-prefetch": false,
                     },
                     class:
                       "font-medium text-blue-600 underline dark:text-blue-500 hover:no-underline cursor-pointer",
diff --git a/app/controllers/projects/samples/attachments_controller.rb b/app/controllers/projects/samples/attachments_controller.rb
index 7d3d32c22c..8b1d7fbfaa 100644
--- a/app/controllers/projects/samples/attachments_controller.rb
+++ b/app/controllers/projects/samples/attachments_controller.rb
@@ -5,6 +5,7 @@ module Samples
     # Controller actions for Project Samples Attachments
     class AttachmentsController < Projects::Samples::ApplicationController
       before_action :attachment, only: %i[destroy]
+      before_action :new_destroy_params, only: %i[new_destroy]
 
       def new
         authorize! @project, to: :update_sample?
@@ -38,6 +39,15 @@ def create
         end
       end
 
+      def new_destroy
+        authorize! @sample, to: :destroy_attachment?
+        render turbo_stream: turbo_stream.update('sample_modal',
+                                                 partial: 'delete_attachment_modal',
+                                                 locals: {
+                                                   open: true
+                                                 }), status: :ok
+      end
+
       def destroy # rubocop:disable Metrics/MethodLength
         authorize! @sample, to: :destroy_attachment?
 
@@ -70,6 +80,11 @@ def attachment
         @attachment = @sample.attachments.find_by(id: params[:id]) || not_found
       end
 
+      def new_destroy_params
+        @attachment = Attachment.find_by(id: params[:attachment_id])
+        @sample = @attachment.attachable
+      end
+
       def destroy_status(attachment, count)
         return count == 2 ? :ok : :multi_status if attachment.associated_attachment
 
diff --git a/app/controllers/projects/samples/deletions_controller.rb b/app/controllers/projects/samples/deletions_controller.rb
new file mode 100644
index 0000000000..3844a64d13
--- /dev/null
+++ b/app/controllers/projects/samples/deletions_controller.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+module Projects
+  module Samples
+    # Controller actions for Project Samples Deletions
+    class DeletionsController < Projects::ApplicationController
+      before_action :sample, only: %i[new destroy]
+      before_action :new_dialog_partial, only: :new
+
+      def new
+        authorize! @project, to: :destroy_sample?
+        render turbo_stream: turbo_stream.update('samples_dialog',
+                                                 partial: @partial,
+                                                 locals: {
+                                                   open: true
+                                                 }), status: :ok
+      end
+
+      def destroy # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
+        ::Samples::DestroyService.new(@project, current_user, { sample: @sample }).execute
+
+        respond_to do |format|
+          if @sample.deleted?
+            format.html do
+              flash[:success] = t('.success', sample_name: @sample.name, project_name: @project.namespace.human_name)
+              redirect_to namespace_project_samples_path(format: :html)
+            end
+            format.turbo_stream do
+              render status: :ok, locals: { type: 'success',
+                                            message: t('.success', sample_name: @sample.name,
+                                                                   project_name: @project.namespace.human_name) }
+            end
+          else
+            format.turbo_stream do
+              render status: :unprocessable_entity,
+                     locals: { type: 'alert', message: @sample.errors.full_messages.first }
+            end
+          end
+        end
+      end
+
+      def destroy_multiple
+        samples_to_delete_count = destroy_multiple_params['sample_ids'].count
+
+        deleted_samples_count = ::Samples::DestroyService.new(@project, current_user, destroy_multiple_params).execute
+
+        # No selected samples deleted
+        if deleted_samples_count.zero?
+          render status: :unprocessable_entity, locals: { type: :error, message: t('.no_deleted_samples') }
+        # Partial sample deletion
+        elsif deleted_samples_count.positive? && deleted_samples_count != samples_to_delete_count
+          render status: :multi_status,
+                 locals: { messages: get_multi_status_destroy_multiple_message(deleted_samples_count,
+                                                                               samples_to_delete_count) }
+        # All samples deleted successfully
+        else
+          render status: :ok, locals: { type: :success, message: t('.success') }
+        end
+      end
+
+      private
+
+      def sample
+        # Necessary return for new when deletion_type = 'multiple', as has no params[:sample_id] defined
+        return if params[:deletion_type] == 'multiple'
+
+        @sample = Sample.find_by(id: params[:id] || params[:sample_id], project_id: project.id) || not_found
+      end
+
+      def new_dialog_partial
+        @partial = params['deletion_type'] == 'single' ? 'new_deletion_dialog' : 'new_multiple_deletions_dialog'
+      end
+
+      def destroy_multiple_params
+        params.require(:multiple_deletion).permit(sample_ids: [])
+      end
+
+      def get_multi_status_destroy_multiple_message(deleted_samples_count, samples_to_delete_count)
+        [
+          { type: :success,
+            message: t('.partial_success',
+                       deleted: "#{deleted_samples_count}/#{samples_to_delete_count}") },
+          { type: :error,
+            message: t('.partial_error',
+                       not_deleted: "#{samples_to_delete_count - deleted_samples_count}/#{samples_to_delete_count}") }
+        ]
+      end
+    end
+  end
+end
diff --git a/app/controllers/projects/samples_controller.rb b/app/controllers/projects/samples_controller.rb
index 46731c25e0..8f033eebdf 100644
--- a/app/controllers/projects/samples_controller.rb
+++ b/app/controllers/projects/samples_controller.rb
@@ -5,9 +5,9 @@ module Projects
   class SamplesController < Projects::ApplicationController # rubocop:disable Metrics/ClassLength
     include Metadata
 
-    before_action :sample, only: %i[show edit update destroy view_history_version]
+    before_action :sample, only: %i[show edit update view_history_version]
     before_action :current_page
-    before_action :set_search_params, only: %i[index destroy]
+    before_action :set_search_params, only: %i[index]
     before_action :set_metadata_fields, only: :index
 
     def index
@@ -78,37 +78,6 @@ def update
       end
     end
 
-    def destroy # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
-      ::Samples::DestroyService.new(@sample, current_user).execute
-      @pagy, @samples = pagy(load_samples)
-      @q = load_samples.ransack(params[:q])
-
-      if @sample.deleted?
-        respond_to do |format|
-          format.html do
-            flash[:success] = t('.success', sample_name: @sample.name, project_name: @project.namespace.human_name)
-            redirect_to namespace_project_samples_path(format: :html)
-          end
-          format.turbo_stream do
-            fields_for_namespace(
-              namespace: @project.namespace,
-              show_fields: params[:q] && params[:q][:metadata].to_i == 1
-            )
-            render status: :ok, locals: { type: 'success',
-                                          message: t('.success', sample_name: @sample.name,
-                                                                 project_name: @project.namespace.human_name) }
-          end
-        end
-      else
-        respond_to do |format|
-          format.turbo_stream do
-            render status: :unprocessable_entity,
-                   locals: { type: 'alert', message: @sample.errors.full_messages.first }
-          end
-        end
-      end
-    end
-
     def select
       authorize! @project, to: :sample_listing?
       @samples = []
diff --git a/app/javascript/controllers/infinite_scroll_controller.js b/app/javascript/controllers/infinite_scroll_controller.js
index f1045100a2..1dfa4f13bd 100644
--- a/app/javascript/controllers/infinite_scroll_controller.js
+++ b/app/javascript/controllers/infinite_scroll_controller.js
@@ -6,7 +6,9 @@ export default class extends Controller {
   static targets = ["all", "pageForm", "pageFormContent", "scrollable", "summary"];
   static values = {
     fieldName: String,
-    pagedFieldName: String
+    pagedFieldName: String,
+    singular: String,
+    plural: String
   }
 
   #page = 1;
@@ -27,12 +29,18 @@ export default class extends Controller {
     }
   }
 
-  #replaceCountPlaceholder(){
-    const summary = this.summaryTarget;
-    summary.textContent = summary.textContent.replace(
-      "COUNT_PLACEHOLDER",
-      this.selectionOutlet.getNumSelected(),
-    );
+  #replaceCountPlaceholder() {
+    const numSelected = this.selectionOutlet.getNumSelected()
+    let summary = this.summaryTarget;
+
+    if (numSelected == 1) {
+      summary.textContent = this.singularValue
+    } else {
+      summary.textContent = this.pluralValue.replace(
+        "COUNT_PLACEHOLDER",
+        numSelected
+      )
+    }
   }
 
   #makePagedHiddenInputs() {
@@ -41,7 +49,7 @@ export default class extends Controller {
     const end = this.#page * itemsPerPage;
     const ids = this.allIds.slice(start, end);
 
-    if(ids && ids.length){
+    if (ids && ids.length) {
       const fragment = document.createDocumentFragment();
       for (const id of ids) {
         fragment.appendChild(
@@ -69,7 +77,7 @@ export default class extends Controller {
     this.allTarget.appendChild(fragment);
   }
 
-  clear(){
+  clear() {
     this.selectionOutlet.clear();
   }
 }
diff --git a/app/javascript/controllers/selection_controller.js b/app/javascript/controllers/selection_controller.js
index 0914518a1a..68d90c19b2 100644
--- a/app/javascript/controllers/selection_controller.js
+++ b/app/javascript/controllers/selection_controller.js
@@ -45,7 +45,6 @@ export default class extends Controller {
   }
 
   remove({ params: { id } }) {
-    id = JSON.stringify(id).replaceAll(",", ", ");
     this.#addOrRemove(false, id);
   }
 
diff --git a/app/services/samples/destroy_service.rb b/app/services/samples/destroy_service.rb
index 4c4fa543b8..4ec50cf764 100644
--- a/app/services/samples/destroy_service.rb
+++ b/app/services/samples/destroy_service.rb
@@ -3,18 +3,43 @@
 module Samples
   # Service used to Delete Samples
   class DestroyService < BaseService
-    attr_accessor :sample
+    attr_accessor :sample, :sample_ids, :project
 
-    def initialize(sample, user = nil, params = {})
-      super(user, params.except(:sample, :id))
-      @sample = sample
+    def initialize(project, user = nil, params = {})
+      super(user, params)
+      @project = project
+      @sample = params[:sample] if params[:sample]
+      @sample_ids = params[:sample_ids] if params[:sample_ids]
     end
 
     def execute
-      authorize! sample.project, to: :destroy_sample?
+      authorize! project, to: :destroy_sample?
 
+      sample.nil? ? destroy_multiple : destroy_single
+    end
+
+    private
+
+    def destroy_single
       sample.destroy
 
+      update_metadata_summary(sample)
+    end
+
+    def destroy_multiple
+      samples = Sample.where(id: sample_ids).where(project_id: project.id)
+      samples_to_delete_count = samples.count
+
+      samples = samples.destroy_all
+
+      samples.each do |sample|
+        update_metadata_summary(sample)
+      end
+
+      samples_to_delete_count
+    end
+
+    def update_metadata_summary(sample)
       sample.project.namespace.update_metadata_summary_by_sample_deletion(sample) if sample.deleted?
     end
   end
diff --git a/app/views/projects/samples/attachments/_attachment.html.erb b/app/views/projects/samples/attachments/_attachment.html.erb
index 69209b86b2..2f9ec239cd 100644
--- a/app/views/projects/samples/attachments/_attachment.html.erb
+++ b/app/views/projects/samples/attachments/_attachment.html.erb
@@ -101,22 +101,12 @@
     <% if allowed_to?(:destroy_attachment?, @sample) %>
       <%= link_to(
         t(".delete"),
-        namespace_project_sample_attachment_path(
+        namespace_project_sample_attachment_new_destroy_path(
           sample_id: @sample.id,
-          id: attachment.id
+          attachment_id: attachment.id
         ),
         data: {
-          turbo_method: :delete,
-          action: "turbo:submit-end->selection#remove",
-          "selection-id-param":
-            (
-              if attachment.associated_attachment
-                [attachment.id, attachment.associated_attachment.id]
-              else
-                attachment.id
-              end
-            ),
-          turbo_confirm: t(".delete_confirm")
+          "turbo-prefetch": false,
         },
         class:
           "font-medium text-blue-600 underline dark:text-blue-500 hover:no-underline cursor-pointer"
diff --git a/app/views/projects/samples/attachments/_delete_attachment_modal.html.erb b/app/views/projects/samples/attachments/_delete_attachment_modal.html.erb
new file mode 100644
index 0000000000..dde35d0654
--- /dev/null
+++ b/app/views/projects/samples/attachments/_delete_attachment_modal.html.erb
@@ -0,0 +1,42 @@
+<%= viral_dialog(open: open, size: :large) do |dialog| %>
+  <%= dialog.with_header(title: t(".title")) %>
+  <%= dialog.with_section do %>
+
+    <%= turbo_frame_tag("deletion-alert") %>
+
+    <div
+      data-controller="selection"
+      data-selection-delete-id-param="<%= @attachment.id %>"
+      data-selection-storage-key-value='<%="files-#{@sample.id}" %>'
+      class="mb-4 font-normal text-slate-500 dark:text-slate-400 overflow-x-visible">
+      <p class="mb-4">
+        <%= t(".description") %>
+      </p>
+      <%= form_for(:deletion, url: namespace_project_sample_attachment_path(id: @attachment.id), method: :delete) do |form| %>
+        <%= form.submit t(".submit_button"),
+                    class: "
+                      button
+                      text-sm
+                      px-5
+                      py-2.5
+                      text-white
+                      bg-red-700
+                      border-red-800
+                      focus:outline-none
+                      hover:bg-red-800
+                      focus:ring-red-300
+                      dark:focus:ring-red-700
+                      dark:bg-red-600
+                      dark:text-white
+                      dark:border-red-600
+                      dark:hover:bg-red-700",
+                    data: {
+                      turbo_frame: "_top",
+                      action: "click->selection#remove",
+                      "selection-id-param": @attachment.id
+                    } %>
+        </div>
+      <% end %>
+    </div>
+  <% end %>
+<% end %>
diff --git a/app/views/projects/samples/attachments/destroy.turbo_stream.erb b/app/views/projects/samples/attachments/destroy.turbo_stream.erb
index 30a7ba0257..fccffe1bb9 100644
--- a/app/views/projects/samples/attachments/destroy.turbo_stream.erb
+++ b/app/views/projects/samples/attachments/destroy.turbo_stream.erb
@@ -1,14 +1,27 @@
 <% if destroyed_attachments %>
+  <%= turbo_stream.update "sample_modal",
+                      partial: "delete_attachment_modal",
+                      locals: {
+                        open: false,
+                      } %>
+
   <% destroyed_attachments.each do |attachment| %>
     <%= turbo_stream.append "flashes" do %>
       <%= viral_flash(
         type: :success,
-        data: t(".success", filename: attachment.file.filename)
+        data: t(".success", filename: attachment.file.filename),
       ) %>
     <% end %>
-
-    <%= turbo_stream.remove dom_id(attachment) %>
   <% end %>
+
+  <%= turbo_stream.update(
+    "table-listing",
+    partial: "projects/samples/attachments/table",
+    locals: {
+      attachments: @sample.attachments,
+    },
+  ) %>
+
 <% else %>
   <%= turbo_stream.append "flashes" do %>
     <%= viral_flash(type: :error, data: message) %>
diff --git a/app/views/projects/samples/clones/_dialog.html.erb b/app/views/projects/samples/clones/_dialog.html.erb
index fe0ab86393..9ef96b0676 100644
--- a/app/views/projects/samples/clones/_dialog.html.erb
+++ b/app/views/projects/samples/clones/_dialog.html.erb
@@ -7,6 +7,8 @@
         data-infinite-scroll-selection-outlet='#samples-table'
         data-infinite-scroll-field-name-value="clone[sample_ids][]"
         data-infinite-scroll-paged-field-name-value="sample_ids[]"
+        data-infinite-scroll-singular-value="<%= t(".description.singular") %>"
+        data-infinite-scroll-plural-value="<%= t(".description.plural") %>"
       >
         <%= form_with(
         url: list_namespace_project_samples_path,
@@ -22,7 +24,7 @@
               text-base leading-relaxed text-slate-500 dark:text-slate-400
             "
           >
-            <%= t(".description") %>
+            <%= t(".description.zero") %>
           </p>
           <div>
             <div class="block mb-1 text-sm font-medium text-slate-900 dark:text-white">
diff --git a/app/views/projects/samples/deletions/_new_deletion_dialog.html.erb b/app/views/projects/samples/deletions/_new_deletion_dialog.html.erb
new file mode 100644
index 0000000000..5a461edb33
--- /dev/null
+++ b/app/views/projects/samples/deletions/_new_deletion_dialog.html.erb
@@ -0,0 +1,40 @@
+<%= viral_dialog(open: open, size: :large) do |dialog| %>
+  <%= dialog.with_header(title: t(".title")) %>
+  <%= dialog.with_section do %>
+
+    <%= turbo_frame_tag("deletion-alert") %>
+
+    <div
+      data-controller="selection"
+      data-selection-delete-id-param="<%= @sample.id %>"
+      class="mb-4 font-normal text-slate-500 dark:text-slate-400 overflow-x-visible">
+      <p class="mb-4">
+        <%= t(".description", sample_name: @sample.name) %>
+      </p>
+      <%= form_for(:deletion, url: namespace_project_samples_deletion_path(sample_id: @sample.id), method: :delete) do |form| %>
+        <%= form.submit t(".submit_button"),
+                    class: "
+                      button
+                      text-sm
+                      px-5
+                      py-2.5
+                      text-white
+                      bg-red-700
+                      border-red-800
+                      focus:outline-none
+                      hover:bg-red-800
+                      focus:ring-red-300
+                      dark:focus:ring-red-700
+                      dark:bg-red-600
+                      dark:text-white
+                      dark:border-red-600
+                      dark:hover:bg-red-700",
+                    data: {
+                      action: "click->selection#remove",
+                      "selection-id-param": @sample.id
+                    } %>
+        </div>
+      <% end %>
+    </div>
+  <% end %>
+<% end %>
diff --git a/app/views/projects/samples/deletions/_new_multiple_deletions_dialog.html.erb b/app/views/projects/samples/deletions/_new_multiple_deletions_dialog.html.erb
new file mode 100644
index 0000000000..46bceef85a
--- /dev/null
+++ b/app/views/projects/samples/deletions/_new_multiple_deletions_dialog.html.erb
@@ -0,0 +1,95 @@
+<%= viral_dialog(open: open) do |dialog| %>
+  <%= dialog.with_header(title: t(".title")) %>
+  <%= dialog.with_section do %>
+    <div
+      data-controller="infinite-scroll"
+      data-infinite-scroll-selection-outlet='#samples-table'
+      data-infinite-scroll-field-name-value="multiple_deletion[sample_ids][]"
+      data-infinite-scroll-paged-field-name-value="sample_ids[]"
+      data-infinite-scroll-singular-value="<%= t(".description.singular") %>"
+      data-infinite-scroll-plural-value="<%= t(".description.plural") %>"
+    >
+      <%= form_with(
+        url: list_namespace_project_samples_path,
+        data: { "infinite-scroll-target": "pageForm" }
+      ) do %>
+        <div data-infinite-scroll-target="pageFormContent"></div>
+      <% end %>
+
+      <div class="grid gap-4">
+          <p
+            data-infinite-scroll-target="summary"
+            class="
+              text-base leading-relaxed text-slate-500 dark:text-slate-400
+            "
+          ><%= t(".description.zero") %></p>
+          <div>
+            <div class="block mb-1 text-sm font-medium text-slate-900 dark:text-white">
+              <%= t(".samples") %>
+            </div>
+            <div
+              class="
+                overflow-y-auto max-h-[300px] border border-slate-300 rounded-md block w-full
+                p-2.5 dark:bg-slate-800 dark:border-slate-600
+              "
+              data-action="scroll->infinite-scroll#scroll"
+              data-infinite-scroll-target="scrollable"
+            >
+              <%= turbo_frame_tag "list_select_samples" do %>
+                <%= render partial: "shared/loading/samples_list_skeleton" %>
+              <% end %>
+            </div>
+          </div>
+
+      <%= form_for(:deletion, url: destroy_multiple_namespace_project_samples_deletion_path, method: :delete,
+            data: {
+              controller: "spinner",
+              action:"turbo:submit-start->spinner#submitStart turbo:submit-end->spinner#submitEnd"
+            }
+          ) do |form| %>
+        <div class="grid gap-4">
+          <div class="hidden" data-infinite-scroll-target="all"></div>
+          <div>
+            <%= form.submit t(".submit_button"),
+                        class: "
+                        button
+                        text-sm
+                        px-5
+                        py-2.5
+                        text-white
+                        bg-red-700
+                        border-red-800
+                        focus:outline-none
+                        hover:bg-red-800
+                        focus:ring-red-300
+                        dark:focus:ring-red-700
+                        dark:bg-red-600
+                        dark:text-white
+                        dark:border-red-600
+                        dark:hover:bg-red-700",
+                        data: {
+                          action: "click->infinite-scroll#clear",
+                          "spinner-target": "submit",
+                          "turbo-submits-with": t(:".loading"),
+                        } %>
+          </div>
+        </div>
+      <% end %>
+    </div>
+
+    <div
+      role="status"
+      id="spinner"
+      class="
+        hidden backdrop-blur-sm absolute h-full w-full -translate-x-1/2 -translate-y-1/2
+        top-2/4 left-1/2 grid place-items-center
+      "
+    >
+      <div class="grid place-items-center">
+        <%= viral_icon(name: :loading, classes: "animate-spin text-primary-500") %>
+        <span class="text-black dark:text-white"><%= t(:".spinner") %>.</span>
+      </div>
+    </div>
+
+  <% end %>
+<% end %>
diff --git a/app/views/projects/samples/deletions/destroy.turbo_stream.erb b/app/views/projects/samples/deletions/destroy.turbo_stream.erb
new file mode 100644
index 0000000000..159da3cd9b
--- /dev/null
+++ b/app/views/projects/samples/deletions/destroy.turbo_stream.erb
@@ -0,0 +1,18 @@
+<%= turbo_stream.append "flashes" do %>
+  <%= viral_flash(type: type, data: message) %>
+<% end %>
+
+<% if @sample.deleted? %>
+  <%= turbo_stream.update(
+    "samples_dialog",
+    partial: "new_deletion_dialog",
+    locals: {
+      open: false,
+    },
+  ) %>
+
+  <%= turbo_stream.replace "project_samples_table" do %>
+    <%= turbo_frame_tag "project_samples_table",
+    src: project_samples_path(@project, format: :turbo_stream) %>
+  <% end %>
+<% end %>
diff --git a/app/views/projects/samples/deletions/destroy_multiple.turbo_stream.erb b/app/views/projects/samples/deletions/destroy_multiple.turbo_stream.erb
new file mode 100644
index 0000000000..593731bd15
--- /dev/null
+++ b/app/views/projects/samples/deletions/destroy_multiple.turbo_stream.erb
@@ -0,0 +1,24 @@
+<%= turbo_stream.update(
+  "samples_dialog",
+  partial: "new_multiple_deletions_dialog",
+  locals: {
+    open: false,
+  },
+) %>
+
+<% if defined?(messages) %>
+  <% messages.each do |m| %>
+    <%= turbo_stream.append "flashes" do %>
+      <%= viral_flash(type: m[:type], data: m[:message]) %>
+    <% end %>
+  <% end %>
+<% else %>
+  <%= turbo_stream.append "flashes" do %>
+    <%= viral_flash(type:, data: message) %>
+  <% end %>
+<% end %>
+
+<%= turbo_stream.replace "project_samples_table" do %>
+  <%= turbo_frame_tag "project_samples_table",
+  src: project_samples_path(@project, format: :turbo_stream) %>
+<% end %>
diff --git a/app/views/projects/samples/destroy.turbo_stream.erb b/app/views/projects/samples/destroy.turbo_stream.erb
deleted file mode 100644
index ab99afb691..0000000000
--- a/app/views/projects/samples/destroy.turbo_stream.erb
+++ /dev/null
@@ -1,13 +0,0 @@
-<%= turbo_stream.append "flashes" do %>
-  <%= viral_flash(type: type, data: message) %>
-<% end %>
-
-<% if @sample.deleted? %>
-  <%= turbo_stream.update "project_samples_table",
-                      partial: "table",
-                      locals: {
-                        project: @project,
-                        pagy: @pagy,
-                        samples: @samples,
-                      } %>
-<% end %>
diff --git a/app/views/projects/samples/index.html.erb b/app/views/projects/samples/index.html.erb
index fb89d80026..ec6bc94597 100644
--- a/app/views/projects/samples/index.html.erb
+++ b/app/views/projects/samples/index.html.erb
@@ -58,6 +58,16 @@
         class: "button button--size-default button--state-primary",
         "aria-label": t(".actions.button_add_aria_label") %>
       <% end %>
+      <% if allowed_to?(:destroy_sample?, @project) %>
+       <%= link_to t(".delete_samples_button"),
+        new_namespace_project_samples_deletion_path(deletion_type: 'multiple'),
+        data: {
+          turbo_stream: true,
+          controller: "action-link",
+          action_link_required_value: 1,
+        },
+        class: "button button--size-default button--state-destructive action-link" %>
+      <% end %>
     </div>
   <% end %>
 <% end %>
diff --git a/app/views/projects/samples/show.html.erb b/app/views/projects/samples/show.html.erb
index e066a36251..613fbd7996 100644
--- a/app/views/projects/samples/show.html.erb
+++ b/app/views/projects/samples/show.html.erb
@@ -15,7 +15,7 @@
       <% if allowed_to?(:destroy?, @project) %>
         <%= link_to(
           t("projects.samples.show.remove_button"),
-          namespace_project_sample_path(id: @sample.id, format: :html),
+          namespace_project_samples_deletion_path(sample_id: @sample.id, format: :html),
           data: {
             turbo_method: :delete,
             turbo_confirm: t("projects.samples.show.remove_button_confirmation"),
diff --git a/app/views/projects/samples/transfers/_dialog.html.erb b/app/views/projects/samples/transfers/_dialog.html.erb
index 9b39dd3862..afb8bbfcc8 100644
--- a/app/views/projects/samples/transfers/_dialog.html.erb
+++ b/app/views/projects/samples/transfers/_dialog.html.erb
@@ -7,6 +7,8 @@
         data-infinite-scroll-selection-outlet='#samples-table'
         data-infinite-scroll-field-name-value="transfer[sample_ids][]"
         data-infinite-scroll-paged-field-name-value="sample_ids[]"
+        data-infinite-scroll-singular-value="<%= t(".description.singular") %>"
+        data-infinite-scroll-plural-value="<%= t(".description.plural") %>"
       >
         <%= form_with(
         url: list_namespace_project_samples_path,
@@ -22,7 +24,7 @@
               text-base leading-relaxed text-slate-500 dark:text-slate-400
             "
           >
-            <%= t(".description") %>
+            <%= t(".description.zero") %>
           </p>
           <div>
             <div class="block mb-1 text-sm font-medium text-slate-900 dark:text-white">
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 3f57edca81..5569cc1cec 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1156,6 +1156,7 @@ en:
         import_metadata_button: Import metadata
         clone_button: Clone samples
         create_export_button: Create Export
+        delete_samples_button: Delete Samples
         actions:
           button_add_aria_label: Add new sample button
           dropdown_aria_label: Dropdown options for sample %{sample_name}
@@ -1174,9 +1175,6 @@ en:
           title: Name and Description
         submit_button: Update sample
         cancel_button: Cancel
-      destroy:
-        success: Sample %{sample_name} was successfully removed from project %{project_name}.
-        error: The sample does not exist within the project.
       shared:
         errors:
           ok_button: OK
@@ -1185,7 +1183,10 @@ en:
       transfers:
         dialog:
           title: Transfer Samples
-          description: The following COUNT_PLACEHOLDER samples are about to be transferred, which will result in these samples no longer being accessible in this project. Maintainers can only transfer samples to another project within the same hierarchy.
+          description:
+            zero: No samples have been selected for transfer
+            singular: The following sample is about to be transferred, which will result in the sample no longer being accessible in this project. Maintainers can only transfer samples to another project within the same hierarchy.
+            plural: The following COUNT_PLACEHOLDER samples are about to be transferred, which will result in these samples no longer being accessible in this project. Maintainers can only transfer samples to another project within the same hierarchy.
           samples: Samples
           new_project_id: Project to transfer samples to
           submit_button: Submit
@@ -1201,7 +1202,10 @@ en:
           no_samples_cloned_error: "Samples were not cloned for the following reasons:"
         dialog:
           title: Clone Samples
-          description: The following COUNT_PLACEHOLDER samples are about to be cloned to another project.
+          description:
+            zero: No samples have been selected for cloning
+            singular: The following sample is about to be cloned to another project.
+            plural: The following COUNT_PLACEHOLDER samples are about to be cloned to another project.
           samples: Samples
           new_project_id: Project to clone samples to
           submit_button: Submit
@@ -1282,7 +1286,6 @@ en:
             error: "File %{filename} was not removed due to the following errors: %{errors}"
         attachment:
           delete: Delete
-          delete_confirm: Are you sure that you want to delete this file from the sample?
         create:
           success: File %{filename} was successfully uploaded.
           failure: "File %{filename} was not uploaded due to the following errors: %{errors}"
@@ -1290,6 +1293,10 @@ en:
           success: File %{filename} was successfully removed.
           error: "File %{filename} was not removed due to the following errors: %{errors}"
         error: File %{filename} was not removed.
+        delete_attachment_modal:
+          description: Are you sure that you want to delete this file rom the sample?
+          submit_button: Confirm
+          title: Delete File
       table:
         description: The project samples table contains all samples related to this project
         selectAll: Select / Deselect Samples
@@ -1308,6 +1315,31 @@ en:
         description: 'Paste a list of <kbd>,</kbd> seperated sample names or identifiers'
         apply: Apply filter
         remove_tag: Remove
+      deletions:
+        new_deletion_dialog:
+          title: Delete Sample
+          description: Are you sure you want to delete sample '%{sample_name}'?
+          submit_button: Remove
+        new_multiple_deletions_dialog:
+            description:
+              zero: No samples have been selected for deletion
+              singular: "The following sample has been selected for deletion:"
+              plural: "The following COUNT_PLACEHOLDER samples have been selected for deletion:"
+            id: ID
+            loading: Loading...
+            name: Name
+            samples: Samples
+            spinner: Deleting samples. This may take a while...
+            submit_button: Confirm
+            title: Delete Samples
+        destroy_multiple:
+          no_deleted_samples: Selected samples could not be deleted
+          partial_error: "%{not_deleted} samples could not be deleted"
+          partial_success: "%{deleted} samples were successfully deleted"
+          success: Samples were successfully deleted
+        destroy:
+          success: Sample %{sample_name} was successfully removed from project %{project_name}.
+          error: The sample does not exist within the project.
     workflow_executions:
       cancel:
         error: "Could not cancel workflow %{workflow_name}"
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index a38e65c83d..451e0e3a3e 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -1152,6 +1152,7 @@ fr:
         import_metadata_button: Import metadata
         clone_button: Clone samples
         create_export_button: Create Export
+        delete_samples_button: Delete Samples
         actions:
           button_add_aria_label: Add new sample button
           dropdown_aria_label: Dropdown options for sample %{sample_name}
@@ -1168,9 +1169,6 @@ fr:
           title: Name and Description
         submit_button: Update sample
         cancel_button: Cancel
-      destroy:
-        success: Sample %{sample_name} was successfully removed from project %{project_name}.
-        error: The sample does not exist within the project.
       shared:
         errors:
           ok_button: OK
@@ -1179,7 +1177,10 @@ fr:
       transfers:
         dialog:
           title: Transfer Samples
-          description: The following COUNT_PLACEHOLDER samples are about to be transferred, which will result in these samples no longer being accessible in this project. Maintainers can only transfer samples to another project within the same hierarchy.
+          description:
+            zero: No samples have been selected for transfer
+            singular: The following sample is about to be transferred, which will result in the sample no longer being accessible in this project. Maintainers can only transfer samples to another project within the same hierarchy.
+            plural: The following COUNT_PLACEHOLDER samples are about to be transferred, which will result in these samples no longer being accessible in this project. Maintainers can only transfer samples to another project within the same hierarchy.
           samples: Samples
           new_project_id: Project to transfer samples to
           submit_button: Submit
@@ -1196,7 +1197,10 @@ fr:
           no_samples_cloned_error: "Samples were not cloned for the following reasons:"
         dialog:
           title: Clone Samples
-          description: The following COUNT_PLACEHOLDER samples are about to be cloned to another project.
+          description:
+            zero: No samples have been selected for cloning
+            singular: The following sample is about to be cloned to another project.
+            plural: The following COUNT_PLACEHOLDER samples are about to be cloned to another project.
           samples: Samples
           new_project_id: Project to clone samples to
           submit_button: Submit
@@ -1277,7 +1281,6 @@ fr:
             error: "File %{filename} was not removed due to the following errors: %{errors}"
         attachment:
           delete: Delete
-          delete_confirm: Are you sure that you want to delete this file from the sample?
         create:
           success: File %{filename} was successfully uploaded.
           failure: "File %{filename} was not uploaded due to the following errors: %{errors}"
@@ -1285,6 +1288,10 @@ fr:
           success: File %{filename} was successfully removed.
           error: "File %{filename} was not removed due to the following errors: %{errors}"
         error: File %{filename} was not removed.
+        delete_attachment_modal:
+          description: Are you sure that you want to delete this file rom the sample?
+          submit_button: Confirm
+          title: Delete File
       table:
         description: The project samples table contains all samples related to this project
         selectAll: Select / Deselect Samples
@@ -1303,6 +1310,31 @@ fr:
         description: 'Paste a list of <kbd>,</kbd> seperated sample names or identifiers'
         apply: Apply filter
         remove_tag: Remove
+      deletions:
+        new_deletion_dialog:
+          title: Delete Sample
+          description: Are you sure you want to delete sample '%{sample_name}'?
+          submit_button: Remove
+        new_multiple_deletions_dialog:
+            description:
+              zero: No samples have been selected for deletion
+              singular: "The following sample has been selected for deletion:"
+              plural: "The following COUNT_PLACEHOLDER samples have been selected for deletion:"
+            id: ID
+            loading: Loading...
+            name: Name
+            samples: Samples
+            spinner: Deleting samples. This may take a while...
+            submit_button: Confirm
+            title: Delete Samples
+        destroy_multiple:
+          no_deleted_samples: Selected samples could not be deleted
+          partial_error: "%{not_deleted} samples could not be deleted"
+          partial_success: "%{deleted} samples were successfully deleted"
+          success: Samples were successfully deleted
+        destroy:
+          success: Sample %{sample_name} was successfully removed from project %{project_name}.
+          error: The sample does not exist within the project.
     workflow_executions:
       cancel:
         error: "Could not cancel workflow %{workflow_name}"
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 09734e168b..f03de5bbcd 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -44,6 +44,9 @@
             resource :clone, only: %i[create new]
             resource :transfer, only: %i[create new]
             resource :file_import, module: :metadata, only: %i[create new]
+            resource :deletion, only: %i[destroy new] do
+              delete :destroy_multiple
+            end
           end
         end
         collection do
@@ -59,6 +62,7 @@
               resource :deletion, only: %i[new destroy]
             end
           end
+          get :new_destroy
         end
         resource :metadata, module: :samples, only: %i[new edit destroy] do
           scope module: :metadata, as: :metadata do
diff --git a/test/controllers/projects/samples/attachments_controller_test.rb b/test/controllers/projects/samples/attachments_controller_test.rb
index f74c9c09fd..2eca3257ce 100644
--- a/test/controllers/projects/samples/attachments_controller_test.rb
+++ b/test/controllers/projects/samples/attachments_controller_test.rb
@@ -113,6 +113,21 @@ class AttachmentsControllerTest < ActionDispatch::IntegrationTest
                  as: :turbo_stream
         end
       end
+
+      test 'new_destroy with proper authorization' do
+        get namespace_project_sample_attachment_new_destroy_path(@namespace, @project, @sample1, @attachment1),
+            as: :turbo_stream
+
+        assert_response :success
+      end
+
+      test 'new_destroy without proper authorization' do
+        sign_in users(:ryan_doe)
+        get namespace_project_sample_attachment_new_destroy_path(@namespace, @project, @sample1, @attachment1),
+            as: :turbo_stream
+
+        assert_response :unauthorized
+      end
     end
   end
 end
diff --git a/test/controllers/projects/samples/deletions_controller_test.rb b/test/controllers/projects/samples/deletions_controller_test.rb
new file mode 100644
index 0000000000..44b1780ef5
--- /dev/null
+++ b/test/controllers/projects/samples/deletions_controller_test.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+module Projects
+  class SamplesControllerTest < ActionDispatch::IntegrationTest
+    setup do
+      sign_in users(:john_doe)
+      @sample1 = samples(:sample1)
+      @sample23 = samples(:sample23)
+      @project = projects(:project1)
+      @namespace = groups(:group_one)
+    end
+
+    test 'should destroy sample' do
+      assert_difference('Sample.count', -1) do
+        delete namespace_project_samples_deletion_path(@namespace, @project),
+               params: {
+                 sample_id: @sample1.id
+               }, as: :turbo_stream
+      end
+    end
+
+    test 'should not destroy sample, if it does not belong to the project' do
+      assert_no_difference('Sample.count') do
+        delete namespace_project_samples_deletion_path(@namespace, @project),
+               params: {
+                 sample_id: @sample23.id
+               }, as: :turbo_stream
+      end
+      assert_response :not_found
+    end
+
+    test 'should not destroy sample, if the current user is not allowed to modify the project' do
+      sign_in users(:ryan_doe)
+
+      assert_no_difference('Sample.count') do
+        delete namespace_project_samples_deletion_path(@namespace, @project),
+               params: {
+                 deletion_type: 'single',
+                 sample_id: @sample1.id
+               }
+      end
+
+      assert_response :unauthorized
+    end
+
+    test 'new destroy with single type with proper authorization' do
+      get new_namespace_project_samples_deletion_path(@namespace, @project),
+          params: {
+            deletion_type: 'single',
+            sample_id: @sample1.id
+          }
+
+      assert_response :success
+    end
+
+    test 'new destroy with single type without proper authorization' do
+      sign_in users(:jane_doe)
+      get new_namespace_project_samples_deletion_path(@namespace, @project),
+          params: {
+            deletion_type: 'single',
+            sample_id: @sample1.id
+          }
+
+      assert_response :unauthorized
+    end
+
+    test 'new destroy with multiple type with proper authorization' do
+      get new_namespace_project_samples_deletion_path(@namespace, @project),
+          params: {
+            deletion_type: 'multiple'
+          }
+
+      assert_response :success
+    end
+
+    test 'new destroy with multiple type without proper authorization' do
+      sign_in users(:jane_doe)
+      get new_namespace_project_samples_deletion_path(@namespace, @project),
+          params: {
+            deletion_type: 'multiple'
+          }
+
+      assert_response :unauthorized
+    end
+
+    test 'successfully deleting multiple samples' do
+      sample2 = samples(:sample2)
+      sample30 = samples(:sample30)
+      assert_difference('Sample.count', -3) do
+        delete destroy_multiple_namespace_project_samples_deletion_path(@namespace, @project),
+               params: {
+                 multiple_deletion: {
+                   sample_ids: [@sample1.id, sample2.id, sample30.id]
+                 }
+               }, as: :turbo_stream
+      end
+      assert_response :success
+    end
+
+    test 'partially deleting multiple samples' do
+      sample2 = samples(:sample2)
+      sample30 = samples(:sample30)
+      assert_difference('Sample.count', -3) do
+        delete destroy_multiple_namespace_project_samples_deletion_path(@namespace, @project),
+               params: {
+                 multiple_deletion: {
+                   sample_ids: [@sample1.id, sample2.id, sample30.id, 'invalid_sample_id']
+                 }
+               }, as: :turbo_stream
+      end
+      assert_response :multi_status
+    end
+
+    test 'deleting no samples in destroy_multiple' do
+      assert_no_difference('Sample.count') do
+        delete destroy_multiple_namespace_project_samples_deletion_path(@namespace, @project),
+               params: {
+                 multiple_deletion: {
+                   sample_ids: %w[invalid_sample_id_1 invalid_sample_id_2 invalid_sample_id_3]
+                 }
+               }, as: :turbo_stream
+      end
+      assert_response :unprocessable_entity
+    end
+
+    test 'deleting no samples in destroy_multiple with valid sample ids but do not belong to project' do
+      sample4 = samples(:sample4)
+      sample5 = samples(:sample5)
+      sample6 = samples(:sample6)
+
+      assert_no_difference('Sample.count') do
+        delete destroy_multiple_namespace_project_samples_deletion_path(@namespace, @project),
+               params: {
+                 multiple_deletion: {
+                   sample_ids: [sample4.id, sample5.id, sample6.id]
+                 }
+               }, as: :turbo_stream
+      end
+      assert_response :unprocessable_entity
+    end
+  end
+end
diff --git a/test/controllers/projects/samples_controller_test.rb b/test/controllers/projects/samples_controller_test.rb
index a426476932..fdab264b0d 100644
--- a/test/controllers/projects/samples_controller_test.rb
+++ b/test/controllers/projects/samples_controller_test.rb
@@ -115,52 +115,19 @@ class SamplesControllerTest < ActionDispatch::IntegrationTest
       assert_response :unprocessable_entity
     end
 
-    test 'should destroy sample' do
-      assert_difference('Sample.count', -1) do
-        delete namespace_project_sample_url(@namespace, @project, @sample1),
-               as: :turbo_stream
-      end
-    end
-
-    test 'should not destroy sample, if it does not belong to the project' do
-      delete namespace_project_sample_url(@namespace, @project, @sample23)
-
-      assert_response :not_found
-    end
-
-    test 'should not destroy sample, if the current user is not allowed to modify the project' do
-      sign_in users(:ryan_doe)
-
-      assert_no_difference('Sample.count') do
-        delete namespace_project_sample_url(@namespace, @project, @sample1)
-      end
-
-      assert_response :unauthorized
-    end
-
     test 'show sample history listing' do
-      sign_in users(:john_doe)
-      namespace = groups(:group_one)
-      project = projects(:project1)
-      sample = samples(:sample1)
-
-      sample.create_logidze_snapshot!
+      @sample1.create_logidze_snapshot!
 
-      get namespace_project_sample_path(namespace, project, sample, tab: 'history')
+      get namespace_project_sample_path(@namespace, @project, @sample1, tab: 'history')
 
       assert_response :success
     end
 
     test 'view sample history version' do
-      sign_in users(:john_doe)
-      namespace = groups(:group_one)
-      project = projects(:project1)
-      sample = samples(:sample1)
-
-      sample.create_logidze_snapshot!
+      @sample1.create_logidze_snapshot!
 
-      get namespace_project_sample_view_history_version_path(namespace, project, sample, version: 1,
-                                                                                         format: :turbo_stream)
+      get namespace_project_sample_view_history_version_path(@namespace, @project, @sample1, version: 1,
+                                                                                             format: :turbo_stream)
 
       assert_response :success
     end
diff --git a/test/services/samples/destroy_service_test.rb b/test/services/samples/destroy_service_test.rb
index d06979b30d..956a226e50 100644
--- a/test/services/samples/destroy_service_test.rb
+++ b/test/services/samples/destroy_service_test.rb
@@ -6,12 +6,15 @@ module Samples
   class DestroyServiceTest < ActiveSupport::TestCase
     def setup
       @user = users(:john_doe)
-      @sample = samples(:sample23)
+      @sample1 = samples(:sample1)
+      @sample2 = samples(:sample2)
+      @sample30 = samples(:sample30)
+      @project = projects(:project1)
     end
 
     test 'destroy sample with correct permissions' do
       assert_difference -> { Sample.count } => -1 do
-        Samples::DestroyService.new(@sample, @user).execute
+        Samples::DestroyService.new(@project, @user, { sample: @sample1 }).execute
       end
     end
 
@@ -19,51 +22,109 @@ def setup
       @user = users(:joan_doe)
 
       exception = assert_raises(ActionPolicy::Unauthorized) do
-        Samples::DestroyService.new(@sample, @user).execute
+        Samples::DestroyService.new(@project, @user, { sample: @sample1 }).execute
       end
 
       assert_equal ProjectPolicy, exception.policy
       assert_equal :destroy_sample?, exception.rule
       assert exception.result.reasons.is_a?(::ActionPolicy::Policy::FailureReasons)
-      assert_equal I18n.t(:'action_policy.policy.project.destroy_sample?', name: @sample.project.name),
+      assert_equal I18n.t(:'action_policy.policy.project.destroy_sample?', name: @project.name),
                    exception.result.message
     end
 
     test 'valid authorization to destroy sample' do
-      assert_authorized_to(:destroy_sample?, @sample.project, with: ProjectPolicy,
-                                                              context: { user: @user }) do
-        Samples::DestroyService.new(@sample,
-                                    @user).execute
+      assert_authorized_to(:destroy_sample?, @sample1.project, with: ProjectPolicy,
+                                                               context: { user: @user }) do
+        Samples::DestroyService.new(@project, @user, { sample: @sample1 }).execute
       end
     end
 
-    test 'metadata summary updated after sample deletion' do
+    test 'metadata summary updated after single sample deletion' do
       # Reference group/projects descendants tree:
       # group12 < subgroup12b (project30 > sample 33)
       #    |
       #    ---- < subgroup12a (project29 > sample 32) < subgroup12aa (project31 > sample34 + 35)
-      @group12 = groups(:group_twelve)
-      @subgroup12a = groups(:subgroup_twelve_a)
-      @subgroup12b = groups(:subgroup_twelve_b)
-      @subgroup12aa = groups(:subgroup_twelve_a_a)
-      @project31 = projects(:project31)
-      @sample34 = samples(:sample34)
-
-      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, @project31.namespace.metadata_summary)
-      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, @subgroup12aa.metadata_summary)
-      assert_equal({ 'metadatafield1' => 2, 'metadatafield2' => 2 }, @subgroup12a.metadata_summary)
-      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, @subgroup12b.metadata_summary)
-      assert_equal({ 'metadatafield1' => 3, 'metadatafield2' => 3 }, @group12.metadata_summary)
-
-      assert_no_changes -> { @subgroup12b.reload.metadata_summary } do
-        Samples::DestroyService.new(@sample34, @user).execute
+      group12 = groups(:group_twelve)
+      subgroup12a = groups(:subgroup_twelve_a)
+      subgroup12b = groups(:subgroup_twelve_b)
+      subgroup12aa = groups(:subgroup_twelve_a_a)
+      project31 = projects(:project31)
+      sample34 = samples(:sample34)
+
+      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, project31.namespace.metadata_summary)
+      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, subgroup12aa.metadata_summary)
+      assert_equal({ 'metadatafield1' => 2, 'metadatafield2' => 2 }, subgroup12a.metadata_summary)
+      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, subgroup12b.metadata_summary)
+      assert_equal({ 'metadatafield1' => 3, 'metadatafield2' => 3 }, group12.metadata_summary)
+
+      assert_no_changes -> { subgroup12b.reload.metadata_summary } do
+        Samples::DestroyService.new(project31, @user, { sample: sample34 }).execute
+      end
+
+      assert_equal({}, project31.namespace.reload.metadata_summary)
+      assert_equal({}, subgroup12aa.reload.metadata_summary)
+      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, subgroup12a.reload.metadata_summary)
+      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, subgroup12b.reload.metadata_summary)
+      assert_equal({ 'metadatafield1' => 2, 'metadatafield2' => 2 }, group12.reload.metadata_summary)
+    end
+
+    test 'multiple destroy with multiple samples and correct permissions' do
+      assert_difference -> { Sample.count } => -3 do
+        Samples::DestroyService.new(@project, @user, { sample_ids: [@sample1.id, @sample2.id, @sample30.id] }).execute
+      end
+    end
+
+    test 'destroy samples with incorrect permissions' do
+      user = users(:joan_doe)
+
+      exception = assert_raises(ActionPolicy::Unauthorized) do
+        Samples::DestroyService.new(@project, user, { sample_ids: [@sample1.id, @sample2.id, @sample30.id] }).execute
+      end
+
+      assert_equal ProjectPolicy, exception.policy
+      assert_equal :destroy_sample?, exception.rule
+      assert exception.result.reasons.is_a?(::ActionPolicy::Policy::FailureReasons)
+      assert_equal I18n.t(:'action_policy.policy.project.destroy_sample?', name: @project.name),
+                   exception.result.message
+    end
+
+    test 'metadata summary updated after multiple sample deletion' do
+      # Reference group/projects descendants tree:
+      # group12 < subgroup12b (project30 > sample 33)
+      #    |
+      #    ---- < subgroup12a (project29 > sample 32) < subgroup12aa (project31 > sample34 + 35)
+      group12 = groups(:group_twelve)
+      subgroup12a = groups(:subgroup_twelve_a)
+      subgroup12b = groups(:subgroup_twelve_b)
+      subgroup12aa = groups(:subgroup_twelve_a_a)
+      project30 = projects(:project30)
+      project31 = projects(:project31)
+      sample33 = samples(:sample33)
+      sample34 = samples(:sample34)
+
+      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, project31.namespace.metadata_summary)
+      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, subgroup12aa.metadata_summary)
+      assert_equal({ 'metadatafield1' => 2, 'metadatafield2' => 2 }, subgroup12a.metadata_summary)
+      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, subgroup12b.metadata_summary)
+      assert_equal({ 'metadatafield1' => 3, 'metadatafield2' => 3 }, group12.metadata_summary)
+
+      Samples::TransferService.new(project30, @user).execute(project31.id, [sample33.id])
+
+      assert_equal(
+        { 'metadatafield1' => 2, 'metadatafield2' => 2 }, project31.reload.namespace.metadata_summary
+      )
+
+      assert_equal({ 'metadatafield1' => 2, 'metadatafield2' => 2 }, subgroup12aa.reload.metadata_summary)
+
+      assert_no_changes -> { subgroup12b.reload.metadata_summary } do
+        Samples::DestroyService.new(project31, @user, { sample_ids: [sample33.id, sample34.id] }).execute
       end
 
-      assert_equal({}, @project31.namespace.reload.metadata_summary)
-      assert_equal({}, @subgroup12aa.reload.metadata_summary)
-      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, @subgroup12a.reload.metadata_summary)
-      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, @subgroup12b.reload.metadata_summary)
-      assert_equal({ 'metadatafield1' => 2, 'metadatafield2' => 2 }, @group12.reload.metadata_summary)
+      assert_equal({}, project31.namespace.reload.metadata_summary)
+      assert_equal({}, subgroup12aa.reload.metadata_summary)
+      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, subgroup12a.reload.metadata_summary)
+      assert_equal({}, subgroup12b.reload.metadata_summary)
+      assert_equal({ 'metadatafield1' => 1, 'metadatafield2' => 1 }, group12.reload.metadata_summary)
     end
   end
 end
diff --git a/test/system/projects/samples_test.rb b/test/system/projects/samples_test.rb
index 9d9f9e02a5..72fc8d9882 100644
--- a/test/system/projects/samples_test.rb
+++ b/test/system/projects/samples_test.rb
@@ -122,8 +122,9 @@ class SamplesTest < ApplicationSystemTestCase
         click_on I18n.t('projects.samples.attachments.attachment.delete'), match: :first
       end
 
-      within('#turbo-confirm[open]') do
-        click_button I18n.t(:'components.confirmation.confirm')
+      within('dialog') do
+        assert_text I18n.t('projects.samples.attachments.delete_attachment_modal.description')
+        click_button I18n.t('projects.samples.attachments.delete_attachment_modal.submit_button')
       end
 
       assert_text I18n.t('projects.samples.attachments.destroy.success', filename: 'test_file.fastq')
@@ -166,8 +167,8 @@ class SamplesTest < ApplicationSystemTestCase
         click_on I18n.t('projects.samples.attachments.attachment.delete'), match: :first
       end
 
-      within('#turbo-confirm[open]') do
-        click_button I18n.t(:'components.confirmation.confirm')
+      within('dialog') do
+        click_button I18n.t('projects.samples.attachments.delete_attachment_modal.submit_button')
       end
 
       assert_text I18n.t('projects.samples.attachments.destroy.success', filename: 'TestSample_S1_L001_R1_001.fastq')
@@ -189,8 +190,8 @@ class SamplesTest < ApplicationSystemTestCase
         click_button I18n.t(:'components.confirmation.confirm')
       end
 
-      assert_text I18n.t('projects.samples.destroy.success', sample_name: @sample1.name,
-                                                             project_name: @project.namespace.human_name)
+      assert_text I18n.t('projects.samples.deletions.destroy.success', sample_name: @sample1.name,
+                                                                       project_name: @project.namespace.human_name)
 
       assert_no_selector '#samples-table table tbody tr', text: @sample1.name
       assert_selector 'h1', text: I18n.t(:'projects.samples.index.title'), count: 1
@@ -210,12 +211,13 @@ class SamplesTest < ApplicationSystemTestCase
         click_link 'Remove'
       end
 
-      within('#turbo-confirm[open]') do
-        click_button I18n.t(:'components.confirmation.confirm')
+      within('dialog') do
+        assert_text I18n.t('projects.samples.deletions.new_deletion_dialog.description', sample_name: @sample1.name)
+        click_button I18n.t('projects.samples.deletions.new_deletion_dialog.submit_button')
       end
 
-      assert_text I18n.t('projects.samples.destroy.success', sample_name: @sample1.name,
-                                                             project_name: @project.namespace.human_name)
+      assert_text I18n.t('projects.samples.deletions.destroy.success', sample_name: @sample1.name,
+                                                                       project_name: @project.namespace.human_name)
 
       assert_no_selector '#samples-table table tbody tr', text: @sample1.puid
       assert_no_selector '#samples-table table tbody tr', text: @sample1.name
@@ -226,7 +228,7 @@ class SamplesTest < ApplicationSystemTestCase
       end
     end
 
-    test 'should transfer samples' do
+    test 'should transfer multiple samples' do
       project2 = projects(:project2)
       visit namespace_project_samples_url(@namespace, @project)
       assert_text 'Displaying 3 items'
@@ -236,6 +238,7 @@ class SamplesTest < ApplicationSystemTestCase
       end
       click_link I18n.t('projects.samples.index.transfer_button'), match: :first
       within('span[data-controller-connected="true"] dialog') do
+        assert_text I18n.t('projects.samples.transfers.description.plural').gsub! 'COUNT_PLACEHOLDER', '3'
         within %(turbo-frame[id="list_select_samples"]) do
           samples = @project.samples.pluck(:puid, :name)
           samples.each do |sample|
@@ -248,6 +251,25 @@ class SamplesTest < ApplicationSystemTestCase
       end
     end
 
+    test 'should transfer a single sample' do
+      project2 = projects(:project2)
+      visit namespace_project_samples_url(@namespace, @project)
+      assert_text 'Displaying 3 items'
+      within '#samples-table table tbody' do
+        assert_selector 'tr', count: 3
+        all('input[type="checkbox"]')[0].click
+      end
+      click_link I18n.t('projects.samples.index.transfer_button'), match: :first
+      within('span[data-controller-connected="true"] dialog') do
+        assert_text I18n.t('projects.samples.transfers.dialog.description.singular')
+        within %(turbo-frame[id="list_select_samples"]) do
+          assert_text @sample1.name
+        end
+        select project2.full_path, from: I18n.t('projects.samples.transfers.dialog.new_project_id')
+        click_on I18n.t('projects.samples.transfers.dialog.submit_button')
+      end
+    end
+
     test 'should not transfer samples with session storage cleared' do
       project2 = projects(:project2)
       visit namespace_project_samples_url(@namespace, @project)
@@ -440,7 +462,7 @@ class SamplesTest < ApplicationSystemTestCase
       assert_selector 'a', text: I18n.t('projects.samples.index.upload_file'), count: 0
     end
 
-    test 'visiting the index should not allow the current user only edit action' do
+    test 'visiting the index should  not allow the current user only edit action' do
       user = users(:joan_doe)
       login_as user
 
@@ -1893,7 +1915,7 @@ class SamplesTest < ApplicationSystemTestCase
       assert_selector 'a', text: I18n.t('projects.samples.index.clone_button'), count: 0
     end
 
-    test 'should clone samples' do
+    test 'should clone multiple samples' do
       project2 = projects(:project2)
       visit namespace_project_samples_url(@namespace, @project)
       within '#samples-table table tbody' do
@@ -1902,6 +1924,9 @@ class SamplesTest < ApplicationSystemTestCase
       end
       click_link I18n.t('projects.samples.index.clone_button'), match: :first
       within('span[data-controller-connected="true"] dialog') do
+        assert_text I18n.t(
+          'projects.samples.clones.dialog.description.plural'
+        ).gsub! 'COUNT_PLACEHOLDER', '3'
         within %(turbo-frame[id="list_select_samples"]) do
           samples = @project.samples.pluck(:puid, :name)
           samples.each do |sample|
@@ -1915,6 +1940,25 @@ class SamplesTest < ApplicationSystemTestCase
       assert_text I18n.t('projects.samples.clones.create.success')
     end
 
+    test 'should clone single sample' do
+      project2 = projects(:project2)
+      visit namespace_project_samples_url(@namespace, @project)
+      within '#samples-table table tbody' do
+        assert_selector 'tr', count: 3
+        all('input[type="checkbox"]')[0].click
+      end
+      click_link I18n.t('projects.samples.index.clone_button'), match: :first
+      within('span[data-controller-connected="true"] dialog') do
+        assert_text I18n.t('projects.samples.clones.dialog.description.singular')
+        within %(turbo-frame[id="list_select_samples"]) do
+          assert_text @sample1.name
+        end
+        select project2.full_path, from: I18n.t('projects.samples.clones.dialog.new_project_id')
+        click_on I18n.t('projects.samples.clones.dialog.submit_button')
+      end
+      assert_text I18n.t('projects.samples.clones.create.success')
+    end
+
     test 'should not clone samples with session storage cleared' do
       project2 = projects(:project2)
       visit namespace_project_samples_url(@namespace, @project)
@@ -2098,5 +2142,161 @@ def retrieve_puids
       end
       puids
     end
+
+    test 'delete multiple samples' do
+      visit namespace_project_samples_url(@namespace, @project)
+      within '#samples-table table tbody' do
+        assert_selector 'tr', count: 3
+        assert_text @sample1.name
+        assert_text @sample2.name
+        assert_text @sample3.name
+        all('input[type=checkbox]').each { |checkbox| checkbox.click unless checkbox.checked? }
+      end
+      click_link I18n.t('projects.samples.index.delete_samples_button'), match: :first
+      within('span[data-controller-connected="true"] dialog') do
+        assert_text I18n.t('projects.samples.deletions.new_multiple_deletions_dialog.title')
+        assert_text I18n.t(
+          'projects.samples.deletions.new_multiple_deletions_dialog.description.plural'
+        ).gsub! 'COUNT_PLACEHOLDER', '3'
+        assert_text @sample1.name
+        assert_text @sample1.puid
+        assert_text @sample2.name
+        assert_text @sample2.puid
+        assert_text @sample3.name
+        assert_text @sample3.puid
+
+        click_on I18n.t('projects.samples.deletions.new_multiple_deletions_dialog.submit_button')
+      end
+      assert_text I18n.t('projects.samples.deletions.destroy_multiple.success')
+
+      within '#project_samples_table' do
+        assert_no_selector 'tr'
+        assert_no_text @sample1.name
+        assert_no_text @sample2.name
+        assert_no_text @sample3.name
+        assert_text I18n.t('projects.samples.index.no_samples')
+      end
+    end
+
+    test 'delete single sample with checkbox and delete samples button' do
+      visit namespace_project_samples_url(@namespace, @project)
+      within '#samples-table table tbody' do
+        assert_selector 'tr', count: 3
+        assert_text @sample1.name
+        assert_text @sample2.name
+        assert_text @sample3.name
+        within 'table tbody tr:first-child' do
+          all('input[type="checkbox"]')[0].click
+        end
+      end
+      click_link I18n.t('projects.samples.index.delete_samples_button'), match: :first
+      within('span[data-controller-connected="true"] dialog') do
+        assert_text I18n.t('projects.samples.deletions.new_multiple_deletions_dialog.title')
+        assert_text I18n.t('projects.samples.deletions.new_multiple_deletions_dialog.description.singular',
+                           sample_name: @sample1.name)
+        within %(turbo-frame[id="list_select_samples"]) do
+          assert_text @sample1.puid
+        end
+
+        click_on I18n.t('projects.samples.deletions.new_multiple_deletions_dialog.submit_button')
+      end
+
+      within '#samples-table table tbody' do
+        assert_selector 'tr', count: 2
+        assert_no_text @sample1.name
+        assert_text @sample2.name
+        assert_text @sample3.name
+      end
+
+      assert_text I18n.t('projects.samples.deletions.destroy_multiple.success')
+    end
+
+    test 'delete single sample with remove link while all samples selected followed by multiple deletion' do
+      visit namespace_project_samples_url(@namespace, @project)
+      within '#samples-table table tbody' do
+        assert_selector 'tr', count: 3
+        assert_text @sample1.name
+        assert_text @sample2.name
+        assert_text @sample3.name
+        all('input[type=checkbox]').each { |checkbox| checkbox.click unless checkbox.checked? }
+      end
+
+      assert find('input#select-all').checked?
+
+      within '#samples-table table tbody tr:first-child' do
+        click_link I18n.t('projects.samples.index.remove_button')
+      end
+
+      within 'dialog' do
+        click_button I18n.t('projects.samples.deletions.new_deletion_dialog.submit_button')
+      end
+
+      within '#samples-table table tbody' do
+        assert_selector 'tr', count: 2
+        assert_no_text @sample1.name
+        assert all('input[type="checkbox"]')[0].checked?
+        assert all('input[type="checkbox"]')[1].checked?
+      end
+
+      assert find('input#select-all').checked?
+
+      click_link I18n.t('projects.samples.index.delete_samples_button'), match: :first
+      within('span[data-controller-connected="true"] dialog') do
+        assert_text I18n.t('projects.samples.deletions.new_multiple_deletions_dialog.title')
+        assert_text I18n.t(
+          'projects.samples.deletions.new_multiple_deletions_dialog.description.plural'
+        ).gsub! 'COUNT_PLACEHOLDER', '2'
+        assert_text @sample2.name
+        assert_text @sample3.name
+        assert_no_text @sample1.name
+        click_on I18n.t('projects.samples.deletions.new_multiple_deletions_dialog.submit_button')
+      end
+      assert_text I18n.t('projects.samples.deletions.destroy_multiple.success')
+
+      within '#project_samples_table' do
+        assert_no_selector 'tr'
+        assert_no_text @sample1.name
+        assert_no_text @sample2.name
+        assert_no_text @sample3.name
+        assert_text I18n.t('projects.samples.index.no_samples')
+      end
+
+      assert_selector 'a.cursor-not-allowed.pointer-events-none', count: 5
+    end
+
+    test 'delete single attachment with remove link while all attachments selected followed by multiple deletion' do
+      visit namespace_project_sample_url(@namespace, @project, @sample1)
+
+      within('#attachments-table-body') do
+        assert_link text: I18n.t('projects.samples.attachments.attachment.delete'), count: 2
+        all('input[type=checkbox]').each { |checkbox| checkbox.click unless checkbox.checked? }
+        click_on I18n.t('projects.samples.attachments.attachment.delete'), match: :first
+      end
+
+      within('dialog') do
+        assert_text I18n.t('projects.samples.attachments.delete_attachment_modal.description')
+        click_button I18n.t('projects.samples.attachments.delete_attachment_modal.submit_button')
+      end
+
+      assert_text I18n.t('projects.samples.attachments.destroy.success', filename: 'test_file.fastq')
+      within('#table-listing') do
+        assert_no_text 'test_file.fastq'
+        assert_text 'test_file_A.fastq'
+      end
+
+      click_link I18n.t('projects.samples.show.delete_files_button'), match: :first
+
+      within('dialog') do
+        assert_text 'test_file_A.fastq'
+        assert_no_text 'test_file.fastq'
+        click_button I18n.t('projects.samples.attachments.deletions.modal.submit_button')
+      end
+
+      assert_text I18n.t('projects.samples.attachments.deletions.destroy.success')
+      assert_no_text 'test_file_A.fastq'
+      assert_no_text 'test_file.fastq'
+      assert_text I18n.t('projects.samples.show.no_files')
+      assert_selector 'a.cursor-not-allowed.pointer-events-none', count: 2
+    end
   end
 end