diff --git a/E2E-tests/config/api_config.py b/E2E-tests/config/api_config.py
index 39a0dca51..c98ab3aa1 100644
--- a/E2E-tests/config/api_config.py
+++ b/E2E-tests/config/api_config.py
@@ -1,6 +1,7 @@
 from dataclasses import dataclass
 from omegaconf import MISSING, SI
 from typing import Optional
+from src.partner_chain_rpc import DParam
 
 
 @dataclass
@@ -116,6 +117,8 @@ class NodesApiConfig:
     selected_node: str = MISSING
     node: Node = MISSING
     token_policy_id: str = MISSING
+    d_param_min: Optional[DParam] = None
+    d_param_max: Optional[DParam] = None
     active_transfer_account: TransferAccount = MISSING
     passive_transfer_account: TransferAccount = MISSING
     negative_test_transfer_account: TransferAccount = MISSING
diff --git a/E2E-tests/config/substrate/local_nodes.json b/E2E-tests/config/substrate/local_nodes.json
index a08b988b5..685affcf6 100644
--- a/E2E-tests/config/substrate/local_nodes.json
+++ b/E2E-tests/config/substrate/local_nodes.json
@@ -96,6 +96,14 @@
                 "lock": 817414140,
                 "send": 298945143
             }
+        },
+        "d_param_min": {
+            "permissioned_candidates_number": 2,
+            "trustless_candidates_number": 2
+        },
+        "d_param_max": {
+            "permissioned_candidates_number": 5,
+            "trustless_candidates_number": 5
         }
     },
     "block_encoding_suffix_grandpa": "3903",
diff --git a/E2E-tests/config/substrate/staging_nodes.json b/E2E-tests/config/substrate/staging_nodes.json
index da1afc072..daf531015 100644
--- a/E2E-tests/config/substrate/staging_nodes.json
+++ b/E2E-tests/config/substrate/staging_nodes.json
@@ -141,6 +141,14 @@
                 "lock": 817414141,
                 "send": 298945143
             }
+        },
+        "d_param_min": {
+            "permissioned_candidates_number": 6,
+            "trustless_candidates_number": 4
+        },
+        "d_param_max": {
+            "permissioned_candidates_number": 6,
+            "trustless_candidates_number": 4
         }
     }
 }
diff --git a/E2E-tests/src/blockchain_api.py b/E2E-tests/src/blockchain_api.py
index 2bdce881d..e37cae623 100644
--- a/E2E-tests/src/blockchain_api.py
+++ b/E2E-tests/src/blockchain_api.py
@@ -120,6 +120,18 @@ def get_authorities(self) -> list:
     def get_status(self):
         pass
 
+    @abstractmethod
+    def update_d_param(self, permissioned_candidates_count: int, trustless_candidates_count: int) -> dict:
+        """
+        Update D parameter configuration for the sidechain
+        Arguments:
+            permissioned_candidates_count {int} -- Number of permissioned candidates
+            trustless_candidates_count {int} -- Number of trustless candidates
+        Returns:
+            (bool, json response) - True/False, and a json response from the sidechain main cli
+        """
+        pass
+
     @abstractmethod
     def register_candidate(self, candidate_name: str) -> (bool, int):
         """
diff --git a/E2E-tests/src/sidechain_main_cli.py b/E2E-tests/src/sidechain_main_cli.py
index 92bcbb4e9..2335be26b 100644
--- a/E2E-tests/src/sidechain_main_cli.py
+++ b/E2E-tests/src/sidechain_main_cli.py
@@ -77,6 +77,31 @@ def get_signatures(
             raise e
         return signatures
 
+    def update_d_param(
+        self,
+        permissioned_candidates_count,
+        registered_candidates_count,
+        payment_key,
+    ):
+        update_d_param_cmd = (
+            f"{self.cli} update-d-parameter "
+            f"--genesis-utxo {self.config.genesis_utxo} "
+            f"--d-parameter-permissioned-candidates-count {permissioned_candidates_count} "
+            f"--d-parameter-registered-candidates-count {registered_candidates_count} "
+            f"--payment-signing-key-file {payment_key} "
+            f"--ogmios-host {self.config.stack_config.ogmios_host} "
+            f"--ogmios-port {self.config.stack_config.ogmios_port} "
+            f"--kupo-host {self.config.stack_config.kupo_host} "
+            f"--kupo-port {self.config.stack_config.kupo_port} "
+            f"--network {self.config.nodes_config.network}"
+        )
+
+        result = self.run_command.run(update_d_param_cmd)
+
+        response = self.handle_response(result)
+
+        return response
+
     def register_candidate(self, signatures: RegistrationSignatures, payment_key, spo_public_key, registration_utxo):
         register_cmd = (
             f"{self.cli} register "
diff --git a/E2E-tests/src/substrate_api.py b/E2E-tests/src/substrate_api.py
index b8733269e..2d98c3964 100644
--- a/E2E-tests/src/substrate_api.py
+++ b/E2E-tests/src/substrate_api.py
@@ -283,8 +283,25 @@ def _read_cardano_key_file(self, filepath):
             logger.error(f"Could not parse cardano key file: {e}")
         return key.strip()
 
-    #################
+    def update_d_param(self, permissioned_candidates_count, registered_candidates_count):
+        signing_key = self.config.nodes_config.governance_authority.mainchain_key
+
+        result = self.sidechain_main_cli.update_d_param(
+            permissioned_candidates_count,
+            registered_candidates_count,
+            signing_key,
+        )
 
+        if result:
+            logger.info(
+                f"Update of D Param of P: {permissioned_candidates_count} and R: {registered_candidates_count} "
+                f" was successful and will take effect in 2 epochs "
+            )
+            return True, result
+        else:
+            return False, None
+
+    #################
     def register_candidate(self, candidate_name):
         keys_files = self.config.nodes_config.nodes[candidate_name].keys_files
         # Get a UTxO from payment account
diff --git a/E2E-tests/tests/committee/test_committee.py b/E2E-tests/tests/committee/test_committee.py
index 8765a208e..fe99a16c7 100644
--- a/E2E-tests/tests/committee/test_committee.py
+++ b/E2E-tests/tests/committee/test_committee.py
@@ -35,6 +35,54 @@ def calculate_d_param_tolerance(pc_epochs_in_mc_epoch, d_param_p, d_param_t):
 
 class TestCommitteeDistribution:
 
+    @mark.committee_distribution
+    @mark.ariadne
+    @mark.xdist_group("governance_action")
+    def test_update_d_param(
+        self,
+        api: BlockchainApi,
+        config: ApiConfig,
+        current_mc_epoch,
+    ):
+        """
+        * get DParam for n + 2 mc epoch
+        * generate new DParam and update it
+        * confirm that DParam was updated
+        """
+        if not config.nodes_config.d_param_min or not config.nodes_config.d_param_max:
+            skip("Cannot test d-param update when min/max parameters are not set")
+
+        p_floor = config.nodes_config.d_param_min.permissioned_candidates_number
+        p_ceil = config.nodes_config.d_param_max.permissioned_candidates_number
+        t_floor = config.nodes_config.d_param_min.trustless_candidates_number
+        t_ceil = config.nodes_config.d_param_max.trustless_candidates_number
+
+        current_d_param = api.get_d_param(current_mc_epoch + 2)
+
+        if (
+            p_floor == p_ceil == current_d_param.permissioned_candidates_number
+            and t_floor == t_ceil == current_d_param.trustless_candidates_number
+        ):
+            skip("Cannot generate new d-param when min and max are equal to current d-param")
+
+        new_d_param = current_d_param
+        while new_d_param == current_d_param:
+            new_d_param = DParam(np.random.randint(p_floor, p_ceil + 1), np.random.randint(t_floor, t_ceil + 1))
+
+        logging.info(f"Updating d-param to {new_d_param}")
+        result = api.update_d_param(new_d_param.permissioned_candidates_number, new_d_param.trustless_candidates_number)
+        assert result, "D-param update failed"
+
+        # FIXME: ETCM-8945 - create and use wait_for_transaction function instead of wait_for_next_pc_block
+        api.wait_for_next_pc_block()
+        actual_d_param = api.get_d_param(current_mc_epoch + 2)
+        actual_p = actual_d_param.permissioned_candidates_number
+        actual_t = actual_d_param.trustless_candidates_number
+        assert (
+            new_d_param.permissioned_candidates_number == actual_p
+            and new_d_param.trustless_candidates_number == actual_t
+        ), "D-param update did not take effect"
+
     @mark.test_key('ETCM-7150')
     @mark.committee_distribution
     @mark.ariadne