diff --git a/app/api/network.py b/app/api/network.py
index 2f728af0d31dfd8f2849115a5fbb4260a7e83d35..584eff344b15877745160c96103d673352bc491a 100644
--- a/app/api/network.py
+++ b/app/api/network.py
@@ -13,7 +13,7 @@ from flask import Blueprint, request
 from flask_login import login_required
 from .. import models
 from ..decorators import login_groups_accepted
-from .utils import get_generic_model, create_generic_model
+from . import utils
 
 bp = Blueprint("network_api", __name__)
 
@@ -25,7 +25,9 @@ def get_scopes():
 
     .. :quickref: Network; Get network scopes
     """
-    return get_generic_model(models.NetworkScope, order_by=models.NetworkScope.name)
+    return utils.get_generic_model(
+        models.NetworkScope, order_by=models.NetworkScope.name
+    )
 
 
 @bp.route("/scopes", methods=["POST"])
@@ -42,7 +44,7 @@ def create_scope():
     :jsonparam domain_id: primary key of the default domain
     :jsonparam description: (optional) description
     """
-    return create_generic_model(
+    return utils.create_generic_model(
         models.NetworkScope,
         mandatory_fields=("name", "first_vlan", "last_vlan", "supernet", "domain_id"),
     )
@@ -55,7 +57,7 @@ def get_networks():
 
     .. :quickref: Network; Get networks
     """
-    return get_generic_model(models.Network, order_by=models.Network.address)
+    return utils.get_generic_model(models.Network, order_by=models.Network.address)
 
 
 @bp.route("/networks", methods=["POST"])
@@ -76,7 +78,7 @@ def create_network():
     :type admin_only: bool
     :jsonparam description: (optional) description
     """
-    return create_generic_model(
+    return utils.create_generic_model(
         models.Network,
         mandatory_fields=(
             "vlan_name",
@@ -105,7 +107,7 @@ def get_interfaces():
             .filter(models.Domain.name == domain)
         )
         query = query.order_by(models.Interface.ip)
-        return get_generic_model(model=None, query=query)
+        return utils.get_generic_model(model=None, query=query)
     network = request.args.get("network", None)
     if network is not None:
         query = models.Interface.query
@@ -113,8 +115,8 @@ def get_interfaces():
             models.Network.vlan_name == network
         )
         query = query.order_by(models.Interface.ip)
-        return get_generic_model(model=None, query=query)
-    return get_generic_model(models.Interface, order_by=models.Interface.ip)
+        return utils.get_generic_model(model=None, query=query)
+    return utils.get_generic_model(models.Interface, order_by=models.Interface.ip)
 
 
 @bp.route("/interfaces", methods=["POST"])
@@ -133,11 +135,23 @@ def create_interface():
     # The validate_interfaces method from the Network class is called when
     # setting interface.network. This is why we don't pass network_id here
     # but network (as vlan_name string)
-    return create_generic_model(
+    return utils.create_generic_model(
         models.Interface, mandatory_fields=("network", "ip", "name")
     )
 
 
+@bp.route("/interfaces/<int:interface_id>", methods=["DELETE"])
+@login_groups_accepted("admin")
+def delete_interface(interface_id):
+    """Delete an interface
+
+    .. :quickref: Network; Delete an interface
+
+    :param interface_id: interface primary key
+    """
+    return utils.delete_generic_model(models.Interface, interface_id)
+
+
 @bp.route("/groups")
 @login_required
 def get_ansible_groups():
@@ -145,7 +159,9 @@ def get_ansible_groups():
 
     .. :quickref: Network; Get Ansible groups
     """
-    return get_generic_model(models.AnsibleGroup, order_by=models.AnsibleGroup.name)
+    return utils.get_generic_model(
+        models.AnsibleGroup, order_by=models.AnsibleGroup.name
+    )
 
 
 @bp.route("/groups", methods=["POST"])
@@ -158,7 +174,7 @@ def create_ansible_groups():
     :jsonparam name: group name
     :jsonparam vars: (optional) Ansible variables
     """
-    return create_generic_model(models.AnsibleGroup, mandatory_fields=("name",))
+    return utils.create_generic_model(models.AnsibleGroup, mandatory_fields=("name",))
 
 
 @bp.route("/hosts")
@@ -168,7 +184,7 @@ def get_hosts():
 
     .. :quickref: Network; Get hosts
     """
-    return get_generic_model(models.Host, order_by=models.Host.name)
+    return utils.get_generic_model(models.Host, order_by=models.Host.name)
 
 
 @bp.route("/hosts", methods=["POST"])
@@ -185,7 +201,21 @@ def create_host():
     :jsonparam ansible_vars: (optional) Ansible variables
     :jsonparam ansible_groups: (optional) list of Ansible groups names
     """
-    return create_generic_model(models.Host, mandatory_fields=("name", "device_type"))
+    return utils.create_generic_model(
+        models.Host, mandatory_fields=("name", "device_type")
+    )
+
+
+@bp.route("/hosts/<int:host_id>", methods=["DELETE"])
+@login_groups_accepted("admin")
+def delete_host(host_id):
+    """Delete a host
+
+    .. :quickref: Network; Delete a host
+
+    :param host_id: host primary key
+    """
+    return utils.delete_generic_model(models.Host, host_id)
 
 
 @bp.route("/macs")
@@ -195,7 +225,7 @@ def get_macs():
 
     .. :quickref: Network; Get mac addresses
     """
-    return get_generic_model(models.Mac, order_by=models.Mac.address)
+    return utils.get_generic_model(models.Mac, order_by=models.Mac.address)
 
 
 @bp.route("/macs", methods=["POST"])
@@ -208,7 +238,7 @@ def create_macs():
     :jsonparam address: MAC address
     :jsonparam item_id: (optional) linked item primary key
     """
-    return create_generic_model(models.Mac, mandatory_fields=("address",))
+    return utils.create_generic_model(models.Mac, mandatory_fields=("address",))
 
 
 @bp.route("/domains")
@@ -218,7 +248,7 @@ def get_domains():
 
     .. :quickref: Network; Get domains
     """
-    return get_generic_model(models.Domain, order_by=models.Domain.name)
+    return utils.get_generic_model(models.Domain, order_by=models.Domain.name)
 
 
 @bp.route("/domains", methods=["POST"])
@@ -230,7 +260,7 @@ def create_domain():
 
     :jsonparam name: domain name
     """
-    return create_generic_model(models.Domain, mandatory_fields=("name",))
+    return utils.create_generic_model(models.Domain, mandatory_fields=("name",))
 
 
 @bp.route("/cnames")
@@ -250,8 +280,8 @@ def get_cnames():
             .filter(models.Domain.name == domain)
         )
         query = query.order_by(models.Cname.name)
-        return get_generic_model(model=None, query=query)
-    return get_generic_model(models.Cname, order_by=models.Cname.name)
+        return utils.get_generic_model(model=None, query=query)
+    return utils.get_generic_model(models.Cname, order_by=models.Cname.name)
 
 
 @bp.route("/cnames", methods=["POST"])
@@ -264,4 +294,6 @@ def create_cname():
     :jsonparam name: full cname
     :jsonparam interface_id: primary key of the associated interface
     """
-    return create_generic_model(models.Cname, mandatory_fields=("name", "interface_id"))
+    return utils.create_generic_model(
+        models.Cname, mandatory_fields=("name", "interface_id")
+    )
diff --git a/app/api/utils.py b/app/api/utils.py
index 6197c01966fb40e1e65d29958a77719f33cb967d..cee5266281b0b4fb2ce3200009cf2a31a8a48bad 100644
--- a/app/api/utils.py
+++ b/app/api/utils.py
@@ -102,3 +102,15 @@ def create_generic_model(model, mandatory_fields=("name",), **kwargs):
     db.session.add(instance)
     commit()
     return jsonify(instance.to_dict()), 201
+
+
+def delete_generic_model(model, primary_key):
+    """Delete the model based on the primary_key
+
+    :param model: model class
+    :param primary_key: primary key of the instance to delete
+    """
+    instance = model.query.get_or_404(primary_key)
+    db.session.delete(instance)
+    db.session.commit()
+    return jsonify(), 204
diff --git a/app/main/views.py b/app/main/views.py
index 5e5171c21588f1990e39a12c5b1f90335106bacc..83d41bd60e4d0e5d5f7522944d4875087ca36417 100644
--- a/app/main/views.py
+++ b/app/main/views.py
@@ -12,7 +12,7 @@ This module implements the main blueprint.
 import os
 import redis
 import rq_dashboard
-from flask import Blueprint, render_template, jsonify, g, current_app, abort
+from flask import Blueprint, render_template, jsonify, g, current_app, abort, request
 from flask_login import login_required, current_user
 from rq import push_connection, pop_connection, Queue
 from ..extensions import sentry
@@ -37,7 +37,14 @@ def forbidden_error(error):
 
 @bp.app_errorhandler(404)
 def not_found_error(error):
-    return render_template("404.html"), 404
+    if (
+        request.path.startswith("/api")
+        or request.accept_mimetypes.best == "application/json"
+    ):
+        # API request - return json
+        return jsonify({"message": "Resource not found"}), 404
+    else:
+        return render_template("404.html"), 404
 
 
 @bp.app_errorhandler(500)
diff --git a/docs/api.rst b/docs/api.rst
index 78539094db3c518df339b69e653ebd949d58c500..a489d506184c2945233c1d8e46f81e102f1f6734 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -39,8 +39,10 @@ Status code             Description
 ======================  ===============================================================================
 :http:statuscode:`200`  The `GET` or `PATCH` request was successfull. The resource is returned as JSON.
 :http:statuscode:`201`  The `POST` request was successful. The created resource is returned as JSON.
+:http:statuscode:`204`  The `DELETE` request was successful. No content is returned.
 :http:statuscode:`401`  Missing Authorization Header.
 :http:statuscode:`403`  The user doesn't have the required permissions.
+:http:statuscode:`404`  A resource could not be accessed, e.g., the ID could not be found.
 :http:statuscode:`422`  The entity could not be processed.
 :http:statuscode:`500`  An error occured while processing the request on the server.
 ======================  ===============================================================================
diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py
index 731f62fc282003774447d3fcb16eccad493ee88d..4360338e5d1aadda4c8870eab199f47be85add8a 100644
--- a/tests/functional/test_api.py
+++ b/tests/functional/test_api.py
@@ -74,6 +74,14 @@ def patch(client, url, data, token=None):
     return response
 
 
+def delete(client, url, token=None):
+    headers = {"Content-Type": "application/json"}
+    if token is not None:
+        headers["Authorization"] = f"Bearer {token}"
+    response = client.delete(url, headers=headers)
+    return response
+
+
 def login(client, username, password):
     data = {"username": username, "password": password}
     return post(client, f"{API_URL}/user/login", data)
@@ -1086,6 +1094,34 @@ def test_create_interface_ip_not_in_range_as_admin(
     assert response.status_code == 201
 
 
+def test_delete_interface_invalid_credentials(client, interface_factory, user_token):
+    interface1 = interface_factory()
+    response = delete(
+        client, f"{API_URL}/network/interfaces/{interface1.id}", token=user_token
+    )
+    assert response.status_code == 403
+    assert len(models.Interface.query.all()) == 1
+
+
+def test_delete_interface_success(client, interface_factory, admin_token):
+    interface1 = interface_factory()
+    response = delete(
+        client, f"{API_URL}/network/interfaces/{interface1.id}", token=admin_token
+    )
+    assert response.status_code == 204
+    assert len(models.Interface.query.all()) == 0
+
+
+def test_delete_interface_invalid_id(client, interface_factory, admin_token):
+    interface1 = interface_factory()
+    response = delete(
+        client, f"{API_URL}/network/hosts/{interface1.id + 1}", token=admin_token
+    )
+    assert response.status_code == 404
+    assert response.get_json() == {"message": "Resource not found"}
+    assert len(models.Interface.query.all()) == 1
+
+
 def test_get_macs(client, mac_factory, readonly_token):
     # Create some macs
     mac1 = mac_factory()
@@ -1327,6 +1363,44 @@ def test_create_host_as_consultant(
     assert response.status_code == 201
 
 
+def test_delete_host_invalid_credentials(client, host_factory, user_token):
+    host1 = host_factory()
+    response = delete(client, f"{API_URL}/network/hosts/{host1.id}", token=user_token)
+    assert response.status_code == 403
+    assert len(models.Host.query.all()) == 1
+
+
+def test_delete_host_success(client, host_factory, admin_token):
+    host1 = host_factory()
+    response = delete(client, f"{API_URL}/network/hosts/{host1.id}", token=admin_token)
+    assert response.status_code == 204
+    assert len(models.Host.query.all()) == 0
+
+
+def test_delete_host_invalid_id(client, host_factory, admin_token):
+    host1 = host_factory()
+    response = delete(
+        client, f"{API_URL}/network/hosts/{host1.id + 1}", token=admin_token
+    )
+    assert response.status_code == 404
+    assert response.get_json() == {"message": "Resource not found"}
+    assert len(models.Host.query.all()) == 1
+
+
+def test_delete_host_with_interfaces(
+    client, interface_factory, host_factory, admin_token
+):
+    interface1 = interface_factory()
+    interface2 = interface_factory()
+    host1 = host_factory(interfaces=[interface1, interface2])
+    assert len(host1.interfaces) == 2
+    assert len(models.Interface.query.all()) == 2
+    response = delete(client, f"{API_URL}/network/hosts/{host1.id}", token=admin_token)
+    assert response.status_code == 204
+    assert len(models.Host.query.all()) == 0
+    assert len(models.Interface.query.all()) == 0
+
+
 def test_get_user_profile(client, readonly_token):
     response = get(client, f"{API_URL}/user/profile", token=readonly_token)
     assert response.status_code == 200