diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index f9623587957d711002c528ccc13941f171e02a09..e42d78f47c5697c1c71104be259b0c3c68678dac 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -16,6 +16,10 @@ class PipelinePolicy < BasePolicy
       enable :update_pipeline
     end
 
+    rule { can?(:owner_access) }.policy do
+      enable :destroy_pipeline
+    end
+
     def ref_protected?(user, project, tag, ref)
       access = ::Gitlab::UserAccess.new(user, project: project)
 
diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..13f892aabb811bb8751d26b0bd53f56652ae4937
--- /dev/null
+++ b/app/services/ci/destroy_pipeline_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+  class DestroyPipelineService < BaseService
+    def execute(pipeline)
+      raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_pipeline, pipeline)
+
+      AuditEventService.new(current_user, pipeline).security_event
+
+      pipeline.destroy!
+    end
+  end
+end
diff --git a/changelogs/unreleased/41875-allow-pipelines-to-be-deleted-by-project-owners.yml b/changelogs/unreleased/41875-allow-pipelines-to-be-deleted-by-project-owners.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0662ff6f52367b8671f15384c4ea89492c5b8f8b
--- /dev/null
+++ b/changelogs/unreleased/41875-allow-pipelines-to-be-deleted-by-project-owners.yml
@@ -0,0 +1,5 @@
+---
+title: Allow deleting a Pipeline via the API.
+merge_request: 22988
+author:
+type: added
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index 574be52801ce5e30b2c71ae46c1a338075baed43..7b4c9a8fbb37bc651cf0700d7bcb8c27c7ccb92e 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -235,5 +235,22 @@ Response:
 }
 ```
 
+## Delete a pipeline
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22988) in GitLab 11.6.
+
+```
+DELETE /projects/:id/pipelines/:pipeline_id
+```
+
+| Attribute  | Type    | Required | Description         |
+|------------|---------|----------|---------------------|
+| `id`       | integer/string | yes      | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `pipeline_id` | integer | yes      | The ID of a pipeline   |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --request "DELETE" "https://gitlab.example.com/api/v4/projects/1/pipelines/46"
+```
+
 [ce-5837]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5837
 [ce-7209]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7209
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index 1cfb982c04ba70962d1e103cf38983b68ec56cdb..cba1e3a668483f4391228bd7fc6436cf3b4d1446 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -81,6 +81,21 @@ class Pipelines < Grape::API
         present pipeline, with: Entities::Pipeline
       end
 
+      desc 'Deletes a pipeline' do
+        detail 'This feature was introduced in GitLab 11.6'
+        http_codes [[204, 'Pipeline was deleted'], [403, 'Forbidden']]
+      end
+      params do
+        requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+      end
+      delete ':id/pipelines/:pipeline_id' do
+        authorize! :destroy_pipeline, pipeline
+
+        destroy_conditionally!(pipeline) do
+          ::Ci::DestroyPipelineService.new(user_project, current_user).execute(pipeline)
+        end
+      end
+
       desc 'Retry builds in the pipeline' do
         detail 'This feature was introduced in GitLab 8.11.'
         success Entities::Pipeline
diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb
index bd32faf06ef746af9b950cc1031476babd6b1236..8022f61e67dd407a10cf9d26f33467270a201bf1 100644
--- a/spec/policies/ci/pipeline_policy_spec.rb
+++ b/spec/policies/ci/pipeline_policy_spec.rb
@@ -74,5 +74,23 @@
         expect(policy).to be_allowed :update_pipeline
       end
     end
+
+    describe 'destroy_pipeline' do
+      let(:project) { create(:project, :public) }
+
+      context 'when user has owner access' do
+        let(:user) { project.owner }
+
+        it 'is enabled' do
+          expect(policy).to be_allowed :destroy_pipeline
+        end
+      end
+
+      context 'when user is not owner' do
+        it 'is disabled' do
+          expect(policy).not_to be_allowed :destroy_pipeline
+        end
+      end
+    end
   end
 end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index f0e1992bccd55c0c7fa2d2d74e0cf262333146d7..638cc9767d4738cad7d706ce2418c429f421959d 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -438,6 +438,67 @@ def expect_variables(variables, expected_variables)
     end
   end
 
+  describe 'DELETE /projects/:id/pipelines/:pipeline_id' do
+    context 'authorized user' do
+      let(:owner) { project.owner }
+
+      it 'destroys the pipeline' do
+        delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner)
+
+        expect(response).to have_gitlab_http_status(204)
+        expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound)
+      end
+
+      it 'returns 404 when it does not exist' do
+        delete api("/projects/#{project.id}/pipelines/123456", owner)
+
+        expect(response).to have_gitlab_http_status(404)
+        expect(json_response['message']).to eq '404 Not found'
+      end
+
+      it 'logs an audit event' do
+        expect { delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) }.to change { SecurityEvent.count }.by(1)
+      end
+
+      context 'when the pipeline has jobs' do
+        let!(:build) { create(:ci_build, project: project, pipeline: pipeline) }
+
+        it 'destroys associated jobs' do
+          delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner)
+
+          expect(response).to have_gitlab_http_status(204)
+          expect { build.reload }.to raise_error(ActiveRecord::RecordNotFound)
+        end
+      end
+    end
+
+    context 'unauthorized user' do
+      context 'when user is not member' do
+        it 'should return a 404' do
+          delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
+
+          expect(response).to have_gitlab_http_status(404)
+          expect(json_response['message']).to eq '404 Project Not Found'
+        end
+      end
+
+      context 'when user is developer' do
+        let(:developer) { create(:user) }
+
+        before do
+          project.add_developer(developer)
+        end
+
+        it 'should return a 403' do
+          delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", developer)
+
+          expect(response).to have_gitlab_http_status(403)
+          expect(json_response['message']).to eq '403 Forbidden'
+        end
+      end
+    end
+  end
+
   describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do
     context 'authorized user' do
       let!(:pipeline) do
diff --git a/spec/services/ci/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..097daf67feb5360dac6d97a2996327f6bb409829
--- /dev/null
+++ b/spec/services/ci/destroy_pipeline_service_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Ci::DestroyPipelineService do
+  let(:project) { create(:project) }
+  let!(:pipeline) { create(:ci_pipeline, project: project) }
+
+  subject { described_class.new(project, user).execute(pipeline) }
+
+  context 'user is owner' do
+    let(:user) { project.owner }
+
+    it 'destroys the pipeline' do
+      subject
+
+      expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound)
+    end
+
+    it 'logs an audit event' do
+      expect { subject }.to change { SecurityEvent.count }.by(1)
+    end
+
+    context 'when the pipeline has jobs' do
+      let!(:build) { create(:ci_build, project: project, pipeline: pipeline) }
+
+      it 'destroys associated jobs' do
+        subject
+
+        expect { build.reload }.to raise_error(ActiveRecord::RecordNotFound)
+      end
+
+      it 'destroys associated stages' do
+        stages = pipeline.stages
+
+        subject
+
+        expect(stages).to all(raise_error(ActiveRecord::RecordNotFound))
+      end
+
+      context 'when job has artifacts' do
+        let!(:artifact) { create(:ci_job_artifact, :archive, job: build) }
+
+        it 'destroys associated artifacts' do
+          subject
+
+          expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+        end
+      end
+    end
+  end
+
+  context 'user is not owner' do
+    let(:user) { create(:user) }
+
+    it 'raises an exception' do
+      expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
+    end
+  end
+end